<!--
Copyright 2015 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
file except in compliance with the License. You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language governing
permissions and limitations under the License.
-->
<!--
The 'google-map-storyboard' renders a Google Map with 'google-map-scene' elements.

<b>Example</b> Add scenes to the map and traverse them linearly:

    <style>
      google-map-storyboard {
        height: 600px;
      }
    </style>
    <google-map-storyboard apiKey="YOUR_KEY_HERE">
      <google-map-scene address="Sydney, Australia" zoom="5">
        <div>This is Sydney!</div>
        <img src="image.png">
      </google-map-scene>
      <google-map-scene address="Zurich, Switzerland"></google-map-scene>
    </google-map-storyboard>


@element google-map-storyboard
@homepage https://github.com/googlemaps/google-map-storyboard
-->
<!--
Fired when the storybord's google map is ready to be rendered.

@event google-map-storyboard-ready
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../google-apis/google-apis.html">
<link rel="import" href="../core-icons/av-icons.html">
<link rel="import" href="../core-icon-button/core-icon-button.html">
<link rel="import" href="google-map-scene.html">
<script src="TransitionManager.js"></script>

<polymer-element name="google-map-storyboard" attributes="apiKey current showMarkers disableDefaultUI">
<template>
  <style>

    :host {
      position: relative;
      display: block;
      height: 100%;
    }

    #map {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
    }

    #controls {
      display: inline-block;
      position: absolute;
      bottom: 0;

      margin-bottom: 24px;
      padding: 8px;
      background: rgba(255, 255, 255, 0.75);
      border-radius: 4px;

      visibility: hidden;
    }

    #map #controls {
      visibility: visible;  /* hidden until added to map */
    }

  </style>

  <google-maps-api apiKey="{{apiKey}}" version="3.exp" libraries="places,geometry" on-api-load="{{mapAPILoaded}}"></google-maps-api>

  <div id="map"></div>
  <div id="controls"></div>

  <content id="scenes" select="google-map-scene" on-scene-changed="{{onSceneChanged}}"></content>
</template>


<script>
(function() {
  Polymer({

    /**
     * A MVCArray of google-map-scenes contained within this storyboard.
     *
     * @property {validScenes}
     * @type {google.maps.MVCArray}
     * @default {null}
     */
    validScenes: null,

    /**
     * The Google Map used by this storyboard.
     *
     * @property {map}
     * @type {google.maps.Map}
     * @default {null}
     */
    map: null,

    /**
     * The Google Maps API key. To obtain an API key,
     * see developers.google.com/maps/documentation/javascript/tutorial#api_key.
     * A key is required for Storyboard to work correctly.
     *
     * @attribute {apiKey}
     * @type {string}
     * @default {null}
     */
    apiKey: null,

    publish: {
      /**
       * The id of the scene to be shown.
       *
       * @attribute {current}
       * @type {?string}
       * @default {null}
       */
      current: {value: null, reflect: true}
    },

    /**
     * If true, there is a marker shown at each scene.
     *
     * @attribute {showMarkers}
     * @type {boolean}
     * @default {true}
     */
    showMarkers: true,

    /**
     * If true, removes the map's default UI controls.
     *
     * @attribute {disableDefaultUI}
     * @type {boolean}
     * @default {true}
     */
    disableDefaultUI: true,

    /**
     * The current scene. Initialised to the first scene.
     *
     * @property {currentScene}
     * @type {google-map-scene}
     * @default {null}
     */
    currentScene: null,

    /**
     * The storyboard's transition and animation manager.
     *
     * @property {transitionManager}
     * @type {LinearAnimationManager}
     * @default {null}
     */
    transitionManager: null,

    initializeMap: function() {
      if (this.map) {
        return;
      }

      var mapOptions = {
        center: this.currentScene.location,
        zoom: this.currentScene.zoom,
        disableDefaultUI: this.disableDefaultUI
      };
      this.map = new google.maps.Map(this.$.map, mapOptions);

      this.prevButton_ = this.makeButton('prev', 'Previous', 'arrow-back');
      this.prevButton_.addEventListener('click', this.previousScene.bind(this));

      this.playButton_ = this.makeButton('pauseAndPlay', 'Pause/Play',
          'av:play-arrow');
      this.playButton_.addEventListener('click', this.pauseAndPlay.bind(this));

      this.nextButton_ = this.makeButton('next', 'Next', 'arrow-forward');
      this.nextButton_.addEventListener('click', this.nextScene.bind(this));

      this.infowindow = new google.maps.InfoWindow;
      this.transitionManager.map = this.map;
      this.showMarkersChanged();
      this.activateScene();
      this.fire('google-map-storyboard-ready');

      this.map.controls[google.maps.ControlPosition.BOTTOM_CENTER].
          push(this.$.controls);
    },

    updateContent: function() {
      if (!this.map) return;
      var currentNode = this.currentScene.firstElementChild;
      var contentString = '';
      var contentNo = 1;
      if (currentNode) {
        contentString = currentNode instanceof HTMLImageElement ?
          currentNode.outerHTML :
          currentNode.textContent;
        this.infowindow.setContent(contentString +
            '<div style="text-align: center"><p>' +
            contentNo + ' of ' +
            this.currentScene.childElementCount + '</p></div>');
        var anchor = this.currentScene.marker || null;
        if (!anchor) {
          this.infowindow.setPosition(this.currentScene.location);
        }
        this.infowindow.open(this.map, anchor);
      }

      google.maps.event.addListener(this.currentScene.marker, 'click',
          changeContent.bind(this));
      function changeContent() {
        if (!this.infowindow.getMap()) {
          if (currentNode) {
            this.infowindow.open(this.map, this.currentScene.marker);
          }
          return;
        }
        if (currentNode.nextElementSibling != null) {
          contentNo += 1;
          currentNode = currentNode.nextElementSibling;
        } else {
          currentNode = this.currentScene.firstElementChild;
          contentNo = 1;
        }
        if (currentNode instanceof HTMLImageElement) {
          this.infowindow.setContent(currentNode.outerHTML +
              '<div style="text-align: center"><p>' +
              contentNo + ' of ' +
              this.currentScene.childElementCount + '</p></div>');
        } else {
          this.infowindow.setContent(currentNode.textContent +
              '<div style="text-align: center"><p>' +
              contentNo + ' of ' +
              this.currentScene.childElementCount + '</p></div>');
        }
      }
    },

    mapAPILoaded: function() {
      if (!this.apiKey || this.apiKey == 'YOUR_KEY_HERE') {
        throw 'You\'re using Storyboard without an API key. Please' +
            ' obtain one at ' +
            'developers.google.com/maps/documentation/javascript/tutorial#api_key';
      }
      this.transitionManager = this.transitionManager ||
          new LinearAnimationManager(this.map);
      this.validScenes = this.validScenes || new google.maps.MVCArray();
      this.initializeScenes();
    },

    initializeScenes: function() {
      var allScenes = Array.prototype.slice.call(
          this.$.scenes.getDistributedNodes());
      this.onMutation(this, this.initializeScenes);
      this.currentScene = this.currentScene || allScenes[0];
      var scenesToUpdate = new google.maps.MVCArray();
      var nextValid = this.validScenes.getAt(0);
      var validSceneIndex = 0;
      var sceneIndex = 0;
      var nextScene = allScenes[0];
      function insertNextScene() {
        if (isValidScene(nextScene)) {
          this.insertScene(validSceneIndex, nextScene);
          ++validSceneIndex;
        }
        scenesToUpdate.push(nextScene);
        nextScene = allScenes[++sceneIndex];
      }
      while (sceneIndex < allScenes.length ||
          validSceneIndex < this.validScenes.length) {
        if (nextValid === nextScene) {
          // The scene is at the right location in the array.
          nextValid = this.validScenes.getAt(++validSceneIndex);
          nextScene = allScenes[++sceneIndex];
        } else if (!nextValid) {
          // There are no more remaining scenes in valid scenes. Add the next
          // scene in allScenes to it.
          insertNextScene.call(this);
        } else if (!nextScene || allScenes.indexOf(nextValid) < 0) {
          // There is no next scene in all scenes, or the scene has been removed
          // from the DOM, so remove the scene from validScenes.
          this.removeScene(validSceneIndex);
          nextValid = this.validScenes.getAt(validSceneIndex);
        } else {
          var indexOfNextSceneInValid = this.validScenes.indexOf(nextScene);
          if (indexOfNextSceneInValid !=
              this.transitionManager.getCurrentIndex()) {
            if (indexOfNextSceneInValid >= 0) {
              this.removeScene(indexOfNextSceneInValid);
            }
            insertNextScene.call(this);
          } else {
            // Remove the scene prior to the current scene. This is so the
            // current scene is kept constant and transitions in progress.
            this.removeScene(validSceneIndex);
            nextValid = this.validScenes.getAt(validSceneIndex);
          }
        }
      }
      scenesToUpdate.forEach(this.updateScene.bind(this));
    },

    onSceneChanged: function(details) {
      // Return early if the maps API hasn't loaded yet.
      if (!this.validScenes) {
        return;
      }
      var scene = details.srcElement;
      this.updateScene(scene);
    },

    removeScene: function(index) {
      index = Math.max(0, Math.min(index, this.validScenes.length - 1));
      // If an update is required due to scene removal, set the current index
      // to the previous scene (or the next, if there is no previous).
      // See LinearAnimationManager.removeAt for more details.
      if (this.transitionManager.isUpdateRequiredIfRemoveAt(index)) {
        var currentIndex = index < 1 ? 1 : (index - 1);
        this.transitionManager.setCurrentIndex(currentIndex,
            this.activateScene.bind(this));
      }
      this.transitionManager.removeAt(index);
      var scene = this.validScenes.removeAt(index);
      scene.setMap(null);
    },

    /**
     * @method {insertScene}
     * @param {number} index The index at which to insert the scene.
     * If it is < 0, calculate the index.
     * @param {google-map-scene} scene
     */
    insertScene: function(index, scene) {
      if (index < 0) {
        index = 0;
        var allScenes = Array.prototype.slice.call(
            this.$.scenes.getDistributedNodes());
        var nextValid = this.validScenes.getAt(0);
        for (var i = 0, nextScene; nextValid, nextScene = allScenes[i],
            nextScene != scene; ++i) {
          if (nextValid === nextScene) {
            nextValid = this.validScenes.getAt(++index);
          }
        }
      }
      this.validScenes.insertAt(index, scene);
      this.transitionManager.insertAt(index, scene.location);
      if (this.showMarkers) scene.setMap(this.map);
      return index;
    },

    updateScene: function(scene) {
      // If there is a new address to geocode, geocode it.
      if (!scene.ignoreAddress) {
        this.geocodeScene(scene);
      }
      // If this is the current scene, and it is invalid, find a replacement.
      if (scene === this.currentScene && isInvalidScene(scene)) {
        this.updateCurrentScene();
      }

      if (!isValidScene(scene)) {
        return;
      }
      var sceneIndex = this.validScenes.indexOf(scene);
      // If the scene is not already in validScenes, insert it.
      if (sceneIndex < 0) {
        sceneIndex = this.insertScene(-1, scene);
      } else {  // Otherwise the scene location has changed.
        this.transitionManager.setAt(sceneIndex, scene.location);
      }

      // If the map is initialized, update the markers and the controls.
      if (!this.map) {
        if (scene != this.currentScene) {
          return;
        }
        this.transitionManager.setCurrentIndex(sceneIndex, null);
        this.initializeMap();
        return;
      }
    },

    geocodeScene: function(scene) {
      if (scene.ignoreAddress || !scene.address) {
        return;
      }
      var address = scene.address.valueOf();
      this.geocoder = this.geocoder || new google.maps.Geocoder();
      this.geocoder.geocode({address: address},
          function(place, status) {
        // Set ignore the address to true if the address hasn't changed.
        scene.ignoreAddress = (address === scene.address);
        if (status == google.maps.GeocoderStatus.OK) {
          scene.location = place[0].geometry.location;
        } else {
          scene.warn('Address %s failed to geocode due to %s. It ' +
              'is shown at location %s.', address, status, scene.location);
        }
      }.bind(this));
    },

    /**
     * If the currentScene is invalid, finds the next (consecutive) non-invalid
     * currentScene.  A scene is non-invalid if the address is being geocoded,
     * or it has a valid location.
     *
     * @method {updateCurrentScene}
     */
    updateCurrentScene: function() {
      if (isValidScene(this.currentScene)) return;
      // Find a new current scene.
      var allScenes = Array.prototype.slice.call(
          this.$.scenes.getDistributedNodes());
      var index = allScenes.indexOf(this.currentScene);
      if (index < 0) index = 0;
      for (var i = 0; i < allScenes.length - 1; ++i) {
        var scene = allScenes[(i + index) % allScenes.length];
        if (!isInvalidScene(scene)) {
          this.currentScene = scene;
          if (scene.location) {
            this.updateScene(scene);
          }
          return;
        }
      }
      this.warn(
          'Warning! All scenes in this storyboard are currently invalid.');
    },

    warn: function() {
      console.warn.apply(console, Array.prototype.slice.call(arguments));
    },

    showMarkersChanged: function() {
      if (!this.map) return;
      var map = (this.showMarkers && this.map) || null;
      this.validScenes.forEach(function(scene) {
        scene.setMap(map);
      });
    },

    disableDefaultUIChanged: function() {
      if (!this.map) return;
      this.map.setOptions({
        disableDefaultUI: this.disableDefaultUI
      });
    },

    currentSceneChanged: function() {
      this.current = this.currentScene.id;
    },

    currentChanged: function() {
      var scene = document.getElementById(this.current);
      if (!scene || scene.element.name != 'google-map-scene') {
        return;
      }
      if (this.validScenes && this.transitionManager) {
        var sceneIndex = this.validScenes.indexOf(scene);
        if (this.map) {
          this.transitionManager.setCurrentIndex(sceneIndex,
                    this.activateScene.bind(this));
        }
      }
      this.currentScene = scene;
    },

    previousScene: function() {
      if (this.transitionManager.hasPrev()) {
        this.infowindow.close();
        this.transitionManager.prev(this.activateScene.bind(this));
        this.updateControls();
      }
    },

    nextScene: function() {
      if (this.transitionManager.hasNext()) {
        this.infowindow.close();
        this.transitionManager.next(this.activateScene.bind(this));
        this.updateControls();
      }
    },

    pauseAndPlay: function() {
      if (this.transitionManager.isAnimating()) {
        this.transitionManager.pause();
      } else if (this.transitionManager.getHeadingOfAnimation() < 0) {
        this.previousScene();
      } else {
        this.nextScene();
      }
      this.updateControls();
    },

    /**
     * Updates the pause and play control.
     *
     * @method {updateControls}
     */
    updateControls: function() {
      var isAnimating = this.transitionManager.isAnimating();
      this.playButton_.icon = isAnimating ? 'av:pause' : 'av:play-arrow';
    },

    /**
     * Activates the upcoming scene.
     *
     * @method {activateScene}
     */
    activateScene: function() {
      if (this.map && this.transitionManager.isIdle()) {
        google.maps.event.clearInstanceListeners(this.currentScene.marker);
        var scene = this.validScenes.getAt(
            this.transitionManager.getCurrentIndex());
        this.map.setZoom(scene.zoom);
        this.map.setCenter(scene.location);
        this.currentScene = scene;
        this.updateControls();
        this.updateContent();
      }
    },

    /**
     * Makes a core-icon-button which is added to the map controls.
     *
     * @method {makeButton}
     * @param {string} id of button
     * @param {string} title to display on title attribute
     * @param {string} icon from core-icons
     * @return {!Element} created button
     */
    makeButton: function(id, title, icon, controlPosition) {
      var button = document.createElement('core-icon-button');
      button.id = id;
      button.title = title;
      button.icon = icon;
      this.$.controls.appendChild(button);
      return button;
    }

  });

  /**
   * Checks if a scene is invalid. A scene is invalid if has both an invalid
   * address and invalid location.
   *
   * @method {isInvalidScene}
   * @param {google-map-scene} scene The scene to check if it is invalid or not.
   * @returns {boolean} True if the scene has an invalid address and location.
   */
  function isInvalidScene(scene) {
    return (scene.ignoreAddress && !scene.location);
  }

  /**
   * Checks if a scene is valid. A scene is valid and if it has a valid
   * location.
   *
   * @method {isValidScene}
   * @param {google-map-scene} scene The scene to check if it is valid or not.
   * @returns {boolean} True if the scene has a valid location.
   */
  function isValidScene(scene) {
    return scene.location instanceof google.maps.LatLng;
  }

})();
</script>
</polymer-element>
